【上一篇】簡單介紹Python內建測試模組unittest,這次我們會繼續討論更多關於單元測試的內容:
TDD為了強調測試的重要性,喊出【先訂定測試案例,再撰寫程式】(Write test cases first),仔細研究,應該是【一邊訂定測試案例,再一邊撰寫程式】,依據【維基百科】的定義,TDD編程的循環(Coding cycle)如下圖:
這樣的作法優點是,不會寫了一堆code之後,發現太多的錯誤,挫折感大增,相對的,以TDD approach進行,類似RUP、動態規劃,將問題分而治之(Divide and conquer),拆分為小問題,逐個解決,最後就能順利解決大問題,筆者在程式教學過程中,利用此一方式效果非常好,例如要實作【9x9乘法表列印】,請學生依序完成下列任務:
i=4
j=5
print(f'{i}x{j}={i*j}')
i=4
for j in range(1,10):
print(f'{i}x{j}={i*j}', end=' ')
for i in range(1,10):
for j in range(1,10):
print(f'{i}x{j}={i*j}', end=' ')
print()
for i in range(1,10):
for j in range(1,10):
print(f'{i}x{j} = {i*j:2d}', end=' ')
print()
以上程式儲存為tdd_tutorial.ipynb。
這種方式確實能幫助初學者無痛的學會基礎程式設計,就TDD而言,可以在每個步驟前,先訂定測試案例,每個步驟後進行單元測試,這種過程對於老手開發簡單的系統可能會覺得很無聊,但對於中大型的系統開發,經實證會比完全不進行單元測試的開發時程來的短,品質提升更不在話下。
Mocking可以在測試環境中模擬真實的物件,假設有一支程式處理出貨程序,貨號是先經過條碼掃描機取得的,但測試環境中可能沒有條碼掃描系統,這時就可以使用Mocking取代。
Python內建模組直接支援Mocking,主要是取代物件,另外,Mock object也支援任意增加方法及屬性,以取代任何型態的物件,以下就以實作代替說明。
範例1. Mock簡單測試,程式修改自【Understanding the Python Mock Object Library】,程式名稱為18\mock_test.py。
import unittest
from unittest.mock import Mock
import requests
from requests.exceptions import Timeout
# 以 Mock 取代原本的 requests
requests = Mock()
def get_holidays():
r = requests.get("http://localhost/api/holidays")
if r.status_code == 200:
return r.json()
return None
class TestHolidays(unittest.TestCase):
def test_get_holidays_timeout(self):
# 測試網路連線
print(get_holidays())
if __name__ == "__main__":
unittest.main()
None
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
class TestHolidays(unittest.TestCase):
def test_get_holidays_timeout(self):
# 設定呼叫 requests.get 時會發生 Timeout 例外
requests.get.side_effect = Timeout
# 若發生 Timeout 例外,執行縮排程式碼
with self.assertRaises(Timeout):
print('call get_holidays')
print(get_holidays())
call get_holidays
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
範例2. Mock的side_effect為一個任何資料型別、陣列、函數或例外,進行以下測試,程式修改自【官方unittest.mock文件】,程式名稱為18\mock_test3.py。
from unittest.mock import Mock
values = {'a': 1, 'b': 2, 'c': 3}
def side_effect(arg):
return values[arg]
mock = Mock()
# side_effect 設為函數
mock.side_effect = side_effect
print(mock('a'), mock('b'), mock('c'))
# side_effect 設為list
mock.side_effect = [5, 4, 3, 2, 1]
print(mock(), mock(), mock())
1 2 3
5 4 3
範例3. Mock的回傳值(return_value)也可以指定,模擬呼叫結果,程式名稱為18\mock_test4.py。
@dataclass
class Response:
status_code:int
json:str
# 以 Mock 取代原本的 requests
requests = Mock()
requests.get.return_value = Response(status_code=200, json={'data':'wondful'})
{'data': 'wondful'}
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
另外,MagicMock支援物件魔術方法(Magic method)的設定,即【_】開頭的內建方法,還支援patch decorator,被指定的物件在測試期間會被替換為 mock,並在測試結束後恢復原狀,可參閱【官方unittest.mock文件】。
覆蓋率分析(Coverage Analysis)是測試的完整性的衡量指標(Metric),計算測試案例佔所有程式所有分支及可能值的比例,是品質保證的重要指標。
Coverage.py可支援Python覆蓋率分析的套件,安裝指令如下:
pip install coverage
範例4. 覆蓋率分析測試,複製\17\project2至18\project2。
coverage run -m unittest discover
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
coverage report -m
coverage html
def add_or_multiply(operator):
if operator == '*':
return 5 * 6
else:
return 5 + 6
coverage run -m unittest discover
coverage report -m
coverage html
def add_or_multiply(operator):
if operator == '*':
return 5 * 6
else:
return 5 + 6
def test_add_or_multiply(self):
self.assertEqual(add_or_multiply('*'), 5*6)
coverage run -m unittest discover
coverage report -m
coverage html
coverage.py的指令及功能非常多,可參閱【Command line usage】,筆者非常喜歡這個套件。
【Python錦囊㊙️技16】展示Django網頁程式完整範例,如果要進行單元測試,Django提供內建測試框架,可參閱【Writing your first Django app, part 5】及【Writing and running tests】,非常容易測試【視圖】(View),它是商業邏輯實作的主要程式碼,程式如下:
切換至16\mysite目錄。
執行下列指令:
python manage.py shell
from django.test.utils import setup_test_environment
setup_test_environment()
from django.test import Client
client = Client()
response = client.get("/")
response.status_code
response.content
執行結果:得到一堆亂碼,因為內碼是UTF-8,而cmd預設內碼是Big5。
可檢視model內容,例如:
response.context["poll_list"]
response.context["poll_list"][0]
response.context["poll_list"][0].pub_date
如果要測試post,可使用client.post(url, data),非常方便。
pytest是一個獨立的套件,與unittest比較,有以下優點:
先安裝套件:
pip install pytest
範例5. 簡單測試,程式名稱為18\test_sample.py,注意,必須為test_開頭的檔名。
import pytest
import pytest
# 測試函數
def func(x):
return x + 1
# 測試案例
def test_answer():
assert func(3) == 5
pytest
================================================ test session starts =================================================
platform win32 -- Python 3.12.6, pytest-8.3.3, pluggy-1.5.0
rootdir: F:\0_python\00_MY\0_ITHome\src\18
plugins: anyio-4.6.0
collected 1 item
test_sample.py F [100%]
====================================================== FAILURES ======================================================
____________________________________________________ test_answer _____________________________________________________
def test_answer():
> assert func(3) == 5
E assert 4 == 5
E + where 4 = func(3)
test_sample.py:9: AssertionError
============================================== short test summary info ===============================================
FAILED test_sample.py::test_answer - assert 4 == 5
================================================= 1 failed in 0.95s ==================================================
筆者初步測試結果並不順暢,pytest與unittest比較,並無明顯優勢,直接以test_開頭的辨識規則,不如unittest以繼承方式來的嚴謹,雖然,pytest有許多擴充套件,個人還是會採用unittest,因此,刪除部份內容說明,避免篇幅過長。
以上只介紹單元測試,不要忘了還有系統整合測試(System Integration Testing, SIT)、使用者驗收測試(User Acceptance Testing, UAT),除了正確性,我們還要考慮壓力測試(Stress testing)、UI/UX,確保系統可承受尖峰的負載以及使用的順暢性,筆者曾經執行基金專案,原本系統很穩定,沒想到系統運作第一天,客戶搬來好幾台類似電腦閱卷的OCR scanner,瞬間輸入大量交易,馬上就把系統灌爆了,真是人算不如天算。
下一篇,我們來討論大型語言模型ChatGPT如何提升開發團隊的生產力。
本系列的程式碼會統一放在GitHub,本篇的程式放在src/18資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。